package org.juiceshop.gm;


import java.util.Map;

import groovy.lang.Closure;


public class Note extends MusicObject implements Comparable<Note> {

	public static final int DEFAULT_PITCH = 60; // Middle-C
	public static final int DEFAULT_DYNAMIC = 64;
	public static final double DEFAULT_VALUE = 1d;
	public static final double DEFAULT_ARTICULATION = 0.9d;
	public static final KeySignature DEFAULT_KEY = KeySignature.major(0);
	
	public static final int REST = Integer.MIN_VALUE;
	
	private static final String[] LETTERS_SHARP  = {"C","C#","D","D#","E","F","F#","G","G#","A","A#","B"};
	private static final String[] LETTERS_FLAT =   {"C","Db","D","Eb","E","F","Gb","G","Ab","A","Hb","B"};
	private static final String[] LETTERS = {"C","","D","","E","F","","G","","A","","B"};
	
	private static String K_DYNAMIC = "dynamic";
	private static String K_ARTICULATION = "articulation";
	private static String K_KEY = "key";
	
	private int _originalPitch = DEFAULT_PITCH;
	private int _accidentials = 0;
	private double _value = DEFAULT_VALUE;
	private boolean _slur = false;
	
	/**
	 * Creates a note with all default values.
	 */
	public Note() {}
	
	/**
	 * Creates a note by note symbol and octave number.
	 * @param name Note symbol - one of <code>"A"</code>, <code>"B"</code> ... <code>"G"</code> or <code>"R"</code>.
	 * 
	 * @param octave Absolute octave number.
	 */
	public Note (String name, int octave) {
		if (Util.isEmpty(name)) {
			throw new IllegalArgumentException("Null or empty symbol");
		}
		String letter = name.substring(0,1);
		String remain = name.substring(1);
		if (letter.equals("R")) {
			_originalPitch = Note.REST;
			return;
		}
		int pitch = Util.findInArray(LETTERS, letter);
		if (pitch<0) throw new IllegalArgumentException("Invalid note letter: "+letter);
		_originalPitch = pitch+octave*12;
		_accidentials = getKey().adjust(pitch) - pitch;
		if (remain.equals("")) {
			// OK
		} else if (remain.equals("#") || remain.equals("sharp")) {
			_accidentials = 1;
		} else if (remain.equals("b") || remain.equals("flat")) {
			_accidentials = -1;
		} else {
			throw new IllegalArgumentException("Invalid note name: "+name);
		}
	}
	
	public Note (Map<String,Object> values, String letter) {
		this(values,letter,DEFAULT_PITCH/12);
	}
	
	public Note (Map<String,Object> values, String letter, int octave) {
		this(letter,octave);
		setValues(values);
	}
	
	public Note (String letter) {
		this(letter,DEFAULT_PITCH/12);
	}
		
	public int getPitch() {
		return _originalPitch+_accidentials;
	}
	
	public void setPitch(int pitch) {
		_originalPitch = pitch;
		_accidentials = 0;
	}

	public int getDynamic() {
		return get(K_DYNAMIC,DEFAULT_DYNAMIC);
	}
	
	@Override
	public double getValue() {
		return _value;
	}
	
	public void setValue(double value) {
		_value = value;
	}

	public double getArticulation() {
		return get(K_ARTICULATION,DEFAULT_ARTICULATION);
	}
	
	public boolean isSlur() {
		return _slur;
	}
	
	public void setSlur(boolean slur) {
		_slur = slur;
	}
	
	public String getSymbol() {
		return getName("#","b");
	}
	
	public String getName() {
		return getName("sharp","flat");
	}
	
	private String getName(final String sharpSymbol,  final String flatSymbol) {
		if (isRest()) return "R";
		int absAccidentials = _accidentials;
		String symbol = sharpSymbol;
		if (_accidentials<0) {
			absAccidentials = -_accidentials;
			symbol = flatSymbol;
		}
		boolean flat = getKey().getMidiSymbol()<0;
		StringBuilder result = new StringBuilder(flat ? LETTERS_FLAT[_originalPitch%12] : LETTERS_SHARP[_originalPitch%12]);
		for (int i=0; i<absAccidentials; i++) {
			result.append(symbol);
		}
		return result.toString();
	}
	
	public KeySignature getKey() {
		return get(K_KEY,DEFAULT_KEY);
	}
	
	
	public double getDuration() {return _value * getArticulation(); }
	
	public int getOctave() { return isRest() ? 0 : getPitch()/12; }
	
	public void setOctave(final int octave) {
		if (!isRest()) {
			setPitch(_originalPitch%12+octave*12);
		}
	}
	
	public boolean isRest() {
		return _originalPitch==REST;
	}
	
	
	/**
	 * Chromatic increment or decrement.
	 * Creates new <code>Note</code> instance witch modified pitch.
	 * @param steps Number of half tones to increment or decrement pitch.
	 * @return new <code>Note</code> instance.
	 */
	public Note step(final int steps) {
		Note copy = copy();
		copy.setPitch(getPitch()+steps);
		return copy;
	}
	
	public Note getSharp() {
		Note note = copy();
		note._accidentials++;
		return note;
	}
	
	public Note getFlat() {
		Note note = copy();
		note._accidentials--;
		return note;
	}
	
	public Note positive() {
		return getSharp();
	}
	
	public Note negative() {
		return getFlat();
	}
		
	public Note bitwiseNegate() {
		if (_accidentials==0) return this;
		return _accidentials<0 ? getSharp() : getFlat();
	}
		
	@Override
	public Note multiply (final double n) {
		Note note = copy();
		note._value *= n;
		return note;
	}
	
	@Override
	public Note div (final double n) {
		Note note = copy();
		note._value /= n;
		return note;
	}
	
	/**
	 * @see {@link #mod(Note)}
	 */
	public Chord mod (final Note note) {
		return new Chord(this,note);
	}
	
	public Chord mod (final int n) {
		return new Chord(this,n);
	}
	
	public Chord getAt(Note...notes) {
		return new Chord(this,notes);
	}
	
	public Chord getAt(int ... numbers) {
		Chord chord = new Chord(this);
		for (int n : numbers) {
			chord.add(this.scale(n));
		}
		return chord;
	}
	
	@Override
	public Phrase or (MusicObject obj) {
		return new Phrase(this).append(obj);
	}
	
	public Phrase xor (Note note) {
		note.setSlur(true);
		return or(note);
	}
	
	public KeySignature getMajor() {
		return KeySignature.major(_originalPitch);
	}
	
	public KeySignature getMinor() {
		return KeySignature.minor(_originalPitch);
	}
	
	@Override
	public Note transform(String method, Object... args) {
		return (Note)invokeMethod(method, args);
	}
	
	@Override
	public Note transform (Closure closure) {
		Object result = closure.call(this);
		if (result instanceof Note) {
			return (Note)result;
		}
		if (result instanceof Map<?,?>) {
			return (Note)copy(Util.asMap(result));
		}
		throw new IllegalArgumentException("Invalid return value from closure: "+result);
	}
	
	
	@Override
	public Note transpose (int steps) {
		Note copy = copy();
		copy._originalPitch += steps;
		return copy;
	}
	
	/**
	 * Diatonic increase or decrease.
	 * Creates a new note that is given full tones higher or lower in
	 * the current scale.
	 * Returns mere copy if the current note is a rest.
	 * @param steps Number of full note values to increase or decrease.
	 * @return New <code>Note</code> instance.
	 */
	public Note scale (int steps) {
		Note copy = copy();
		copy._originalPitch = getKey().scale(_originalPitch, steps);
		return copy;
	}
	
	/**
	 * Number of notes.
	 * Returns always 1.
	 */
	@Override
	public int size() { return 1; }
		
	@Override
	public String toString () {
		return "<"+getSymbol()+"("+getOctave()+")*"+getValue()+">";
	}
		
	/**
	 * Diatonic increase (prefixed <code>++</code>-Operator).
	 * @see #scale(int).
	 */
	public Note next() {
		Note copy = copy();
		copy._originalPitch = getKey().scale(_originalPitch, +1);
		return copy;
	}

	
	/**
	 * Diatonic decrease (prefixed <code>--</code>-Operator).
	 * @see #scale(int).
	 */
	public Note previous() {
		Note copy = copy();
		copy._originalPitch = getKey().scale(_originalPitch, -1);
		return copy;
	}

	/**
	 * Compare note value.
	 */
	@Override
	public int compareTo(Note other) {
		return this.getPitch() - other.getPitch();
	}
	
	@Override
	public Note copy() {
		return (Note)super.copy();
	}
	
	/**
	 * Get property or chord.
	 * Overrides {@link MusicObject#get(String)} method to return
	 * a chord of the given name if there is one registered in {@link Chord} class.
	 */
	@Override
	public Object get(String name) {
		Object result = super.get(name);
		if (result!=null) return result;
		if (Chord.existPattern(name)) {
			return new Chord(this,name);
		}
		return null;
	}

	
}
